Skip to content

Add support for stop(cause:). #388

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 24, 2025
Merged

Add support for stop(cause:). #388

merged 3 commits into from
Jul 24, 2025

Conversation

ioquatix
Copy link
Member

@ioquatix ioquatix commented May 16, 2025

Add cause: to Async::Task#stop(cause:) so that extra information about the reason for stopping can be provided. It must be an exception suitable for raise Async::Stop, cause: cause.

Fixes #387.

Types of Changes

  • New feature.

Contribution

@ioquatix ioquatix force-pushed the async-stop-cause branch 3 times, most recently from 1aee925 to 53670d1 Compare May 16, 2025 05:25
@ioquatix
Copy link
Member Author

I found an issue, Fiber#raise does not seem to support cause: keyword argument correctly. I need to investigate.

@macournoyer
Copy link

I test w/ a sleep, and sending SIGINT to the worker sleeping. Here's what I got:

Async::Stop: {cause: #<Async::Stop: {cause: #<Async::Stop: {cause: #<Async::Stop: {cause: #<Interrupt: Interrupt>}>}>}>}
/Users/ma/.gem/ruby/3.4.4/bundler/gems/async-53670d16cb43/lib/async/scheduler.rb:221:in 'IO::Event::Selector::KQueue#transfer'
	/Users/ma/.gem/ruby/3.4.4/bundler/gems/async-53670d16cb43/lib/async/scheduler.rb:221:in 'Async::Scheduler#block'
	/Users/ma/.gem/ruby/3.4.4/bundler/gems/async-53670d16cb43/lib/async/scheduler.rb:254:in 'Async::Scheduler#kernel_sleep'

Seems there is too much nesting.

@ioquatix
Copy link
Member Author

@samuel-williams-shopify
Copy link
Contributor

samuel-williams-shopify commented Jul 22, 2025

@macournoyer I was able to understand the cause of the output you are seeing.

fiber = Fiber.new do
  while true
    begin
      Fiber.yield
    rescue Exception => error
      puts "error: #{error.inspect}", "error.cause: #{error.cause.inspect}"
    end
  end
end

fiber.resume

# Example of raising an exception with a cause:
fiber.raise StandardError.new("boom"), cause: StandardError.new("cause")

On 3.4, the cause is not correctly attributed to the error and instead ends up being a keyword argument.

> chruby 3.4
> ruby ./test.rb
error: #<StandardError: {cause: #<StandardError: cause>}>
error.cause: nil

On ruby-head, the cause is correctly attributed to the error (including this PR):

> chruby ruby-head
> ruby ./test.rb
error: #<StandardError: boom>
error.cause: #<StandardError: cause>

Deeply nested cause Async::Stop: {cause: #<Async::Stop: {cause: #<Async::Stop: {cause: #<Async::Stop: {cause: #<Interrupt: Interrupt>}>}>}>} is an artefact of several tasks stopping in sequence. The causality chain is not clearly printed but in 3.5+ it should be.

@ioquatix
Copy link
Member Author

ioquatix commented Jul 23, 2025

Another before/after example:

require_relative "lib/async"

Async do
  sleep
ensure
  Console.error(self, "Task exiting", $!)
end

3.4.4 on main (without this PR)

> chruby 3.4.4
> ruby ./test.rb
^C  0.0s    error: Object [oid=0x20] [ec=0x28] [pid=199670] [2025-07-23 18:23:35 +1200]
               | Task exiting
               | Async::Stop
               |   Async::Stop: Async::Stop
               |   → lib/async/scheduler.rb:191 in 'IO::Event::Selector::URing#transfer'
               |     lib/async/scheduler.rb:191 in 'Async::Scheduler#transfer'
               |     lib/async/scheduler.rb:281 in 'Async::Scheduler#kernel_sleep'
               |     ./test.rb:4 in 'Kernel#sleep'
               |     ./test.rb:4 in 'block in <main>'
               |     lib/async/task.rb:205 in 'block in Async::Task#run'
               |     lib/async/task.rb:443 in 'block in Async::Task#schedule'
/home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453:in 'IO::Event::Selector::URing#select': Interrupt
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453:in 'Async::Scheduler#run_once!'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:492:in 'Async::Scheduler#run_once'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:568:in 'block in Async::Scheduler#run'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:528:in 'block in Async::Scheduler#run_loop'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525:in 'Thread.handle_interrupt'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525:in 'Async::Scheduler#run_loop'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:567:in 'Async::Scheduler#run'
	from /home/samuel/Developer/socketry/async/lib/kernel/async.rb:34:in 'Kernel#Async'
  • The error encountered from within the task (Async::Stop) has no cause attached.

3.4.4 + this PR

> chruby 3.4.4
> ruby ./test.rb
^C  0.0s    error: Object [oid=0x20] [ec=0x28] [pid=192112] [2025-07-23 18:12:29 +1200]
               | Task exiting
               | {cause: Interrupt}
               |   Async::Stop: {cause: Interrupt}
               |   → lib/async/scheduler.rb:191 in 'IO::Event::Selector::URing#transfer'
               |     lib/async/scheduler.rb:191 in 'Async::Scheduler#transfer'
               |     lib/async/scheduler.rb:281 in 'Async::Scheduler#kernel_sleep'
               |     ./test.rb:4 in 'Kernel#sleep'
               |     ./test.rb:4 in 'block in <main>'
               |     lib/async/task.rb:237 in 'block in Async::Task#run'
               |     lib/async/task.rb:481 in 'block in Async::Task#schedule'
/home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453:in 'IO::Event::Selector::URing#select': Interrupt
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453:in 'Async::Scheduler#run_once!'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:492:in 'Async::Scheduler#run_once'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:568:in 'block in Async::Scheduler#run'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:528:in 'block in Async::Scheduler#run_loop'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525:in 'Thread.handle_interrupt'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525:in 'Async::Scheduler#run_loop'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:567:in 'Async::Scheduler#run'
	from /home/samuel/Developer/socketry/async/lib/kernel/async.rb:34:in 'Kernel#Async'
	from ./test.rb:3:in '<main>'
  • The error encountered from within the task has an odd keyword argument attached.

HEAD + this PR

> chruby ruby-head
> ruby ./test.rb
^C  0.0s    error: Object [oid=0x20] [ec=0x28] [pid=192193] [2025-07-23 18:12:34 +1200]
               | Task exiting
               | Task was stopped
               |   Async::Stop: Task was stopped
               |   → lib/async/scheduler.rb:191 in 'IO::Event::Selector::URing#transfer'
               |     lib/async/scheduler.rb:191 in 'Async::Scheduler#transfer'
               |     lib/async/scheduler.rb:281 in 'Async::Scheduler#kernel_sleep'
               |     ./test.rb:4 in 'Kernel#sleep'
               |     ./test.rb:4 in 'block in <main>'
               |     lib/async/task.rb:237 in 'block in Async::Task#run'
               |     lib/async/task.rb:481 in 'block in Async::Task#schedule'
               |   Caused by Interrupt: Interrupt
               |   → /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453 in 'IO::Event::Selector::URing#select'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453 in 'Async::Scheduler#run_once!'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:492 in 'Async::Scheduler#run_once'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:568 in 'block in Async::Scheduler#run'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:528 in 'block in Async::Scheduler#run_loop'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525 in 'Thread.handle_interrupt'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525 in 'Async::Scheduler#run_loop'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:567 in 'Async::Scheduler#run'
               |     /home/samuel/Developer/socketry/async/lib/kernel/async.rb:34 in 'Kernel#Async'
               |     ./test.rb:3 in '<main>'
/home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453:in 'IO::Event::Selector::URing#select': Interrupt
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453:in 'Async::Scheduler#run_once!'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:492:in 'Async::Scheduler#run_once'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:568:in 'block in Async::Scheduler#run'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:528:in 'block in Async::Scheduler#run_loop'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525:in 'Thread.handle_interrupt'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525:in 'Async::Scheduler#run_loop'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:567:in 'Async::Scheduler#run'
	from /home/samuel/Developer/socketry/async/lib/kernel/async.rb:34:in 'Kernel#Async'
	from ./test.rb:3:in '<main>'
  • The cause is correctly attached.

HEAD (without this PR)

Interestingly enough, Fiber#raise automatically uses the current exception ($!) as cause, the same as Kernel#raise. Therefore, while this PR has some ergonomic advantages, it's not strictly needed:

> chruby ruby-head
> git checkout main
> ruby ./test.rb
^C  0.0s    error: Object [oid=0x20] [ec=0x28] [pid=201905] [2025-07-23 18:26:59 +1200]
               | Task exiting
               | Async::Stop
               |   Async::Stop: Async::Stop
               |   → lib/async/scheduler.rb:191 in 'IO::Event::Selector::URing#transfer'
               |     lib/async/scheduler.rb:191 in 'Async::Scheduler#transfer'
               |     lib/async/scheduler.rb:281 in 'Async::Scheduler#kernel_sleep'
               |     ./test.rb:4 in 'Kernel#sleep'
               |     ./test.rb:4 in 'block in <main>'
               |     lib/async/task.rb:205 in 'block in Async::Task#run'
               |     lib/async/task.rb:443 in 'block in Async::Task#schedule'
               |   Caused by Interrupt: Interrupt
               |   → /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453 in 'IO::Event::Selector::URing#select'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453 in 'Async::Scheduler#run_once!'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:492 in 'Async::Scheduler#run_once'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:568 in 'block in Async::Scheduler#run'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:528 in 'block in Async::Scheduler#run_loop'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525 in 'Thread.handle_interrupt'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525 in 'Async::Scheduler#run_loop'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:567 in 'Async::Scheduler#run'
               |     /home/samuel/Developer/socketry/async/lib/kernel/async.rb:34 in 'Kernel#Async'
               |     ./test.rb:3 in '<main>'
  • Cause is correctly attached in this case (but there are some cases where it won't be).

@ioquatix
Copy link
Member Author

Okay, the changes to CRuby were merged. We just need to make this work as best we can across different Ruby versions.

@ioquatix ioquatix force-pushed the async-stop-cause branch 2 times, most recently from 0f89936 to 195cf8c Compare July 24, 2025 06:45
@ioquatix ioquatix enabled auto-merge (squash) July 24, 2025 08:30
@ioquatix ioquatix disabled auto-merge July 24, 2025 08:31
@ioquatix ioquatix merged commit e9703fd into main Jul 24, 2025
82 of 111 checks passed
@ioquatix ioquatix deleted the async-stop-cause branch July 24, 2025 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve Async::Stop backtrace
3 participants